package pl.edu.agh.student.vrp;

import org.jgap.*;
import org.jgap.impl.IntegerGene;
import pl.edu.agh.student.vrp.utils.DisplayUtils;

import java.util.ArrayList;
import java.util.List;

/**
 * Krzyzowanie PMX (Partially-Mapped Crossover)
 */
public class PmxCrossover extends BaseGeneticOperator {
    /**
     * Crossover rate in percentage of population size (mutual exclusive to
     * m_crossoverRate and m_crossoverRateCalc).
     */
    private double crossoverRatePercent;

    /**
     * true: also x-over chromosomes with age of zero (newly created chromosomes)
     */
    private boolean xoverNewAge;

    public PmxCrossover(Configuration a_configuration) throws InvalidConfigurationException {
        super(a_configuration);
        xoverNewAge = false;
        crossoverRatePercent = 0.5;
    }

    /**
     * Does the crossing over.
     *
     * @param population the population of chromosomes from the current
     *                   evolution prior to exposure to crossing over
     * @param list       the pool of chromosomes that have been
     *                   selected for the next evolved population
     */
    public void operate(Population population, List list) {
        // Work out the number of crossovers that should be performed.
        int size = Math.min(getConfiguration().getPopulationSize(), population.size());
        int numCrossovers = (int) (size * crossoverRatePercent);
        RandomGenerator generator = getConfiguration().getRandomGenerator();
        IGeneticOperatorConstraint constraint = getConfiguration().
                getJGAPFactory().getGeneticOperatorConstraint();

        // For each crossover, grab two random chromosomes, pick two random
        // gene locations (locus), and then do PMX x-over
        int index1, index2;
        for (int i = 0; i < numCrossovers; i++) {
            index1 = generator.nextInt(size);
            index2 = generator.nextInt(size);
            IChromosome chrom1 = population.getChromosome(index1);
            IChromosome chrom2 = population.getChromosome(index2);

            // Verify that crossover is allowed.
            if (!isXoverNewAge() && chrom1.getAge() < 1 && chrom2.getAge() < 1) {
                // Crossing over two newly created chromosomes is not seen as helpful here.
                continue;
            }
            if (constraint != null) {
                List v = new ArrayList();
                v.add(chrom1);
                v.add(chrom2);
                if (!constraint.isValid(population, v, this)) {
                    // Constraint forbids crossing over.
                    continue;
                }
            }
            // Clone the chromosomes.
            IChromosome firstMate = (IChromosome) chrom1.clone();
            IChromosome secondMate = (IChromosome) chrom2.clone();

            // Cross over the chromosomes.
            doCrossover(firstMate, secondMate, list, generator);
        }
    }

    protected void doCrossover(IChromosome firstMate, IChromosome secondMate,
                               List candidateChromosomes,
                               RandomGenerator generator) {
        Gene[] firstGenes = firstMate.getGenes();
        Gene[] secondGenes = secondMate.getGenes();
        int begin = generator.nextInt(firstGenes.length);
        int end = begin + generator.nextInt(firstGenes.length - begin);
//        System.out.println("BEFORE:");
//        DisplayUtils.displayChromosome(firstMate);
//        DisplayUtils.displayChromosome(secondMate);
//        System.out.printf("- doCrossover(): begin=%d; end=%d\n", begin, end);

        // Swap the genes.
        int firstAllele;
        int secondAllele;

        //iterate through the matched pairs of genes from begin to end swapping the places in each child
        for (int j = begin; j < end; j++) {
            //these are the genes we want to swap
            firstAllele = ((IntegerGene) firstGenes[j]).intValue();
            secondAllele = ((IntegerGene) secondGenes[j]).intValue();

            if (firstAllele != secondAllele && firstAllele != 0 && secondAllele != 0) {
//                System.out.printf("* mapping: %d <-> %d\n", firstAllele, secondAllele);

                //find and swap them in baby1
                swap(firstGenes, firstAllele, secondAllele);

                //and in baby2
                swap(secondGenes, secondAllele, firstAllele);
            }
        }//next pair

        // Add the modified chromosomes to the candidate pool so that
        // they'll be considered for natural selection during the next
        // phase of evolution.
        candidateChromosomes.add(firstMate);
        candidateChromosomes.add(secondMate);

//        System.out.println("AFTER:");
//        DisplayUtils.displayChromosome(firstMate);
//        DisplayUtils.displayChromosome(secondMate);
    }

    protected void swap(Gene[] genes, int originValue, int targetValue) {
        for (Gene g : genes) {
            if (((IntegerGene) g).intValue() == originValue)
                g.setAllele(targetValue);
            else if (((IntegerGene) g).intValue() == targetValue)
                g.setAllele(originValue);
        }
    }

    public int compareTo(Object o) {
        if (o == null) {
            return 1;
        }
        PmxCrossover op = (PmxCrossover) o;
        if (crossoverRatePercent != op.crossoverRatePercent) {
            if (crossoverRatePercent > op.crossoverRatePercent) {
                return 1;
            } else {
                return -1;
            }
        }
        if (xoverNewAge != op.xoverNewAge) {
            if (xoverNewAge) {
                return 1;
            } else {
                return -1;
            }
        }
        return 0;
    }

    public double getCrossoverRatePercent() {
        return crossoverRatePercent;
    }

    public void setCrossoverRatePercent(double crossoverRatePercent) {
        this.crossoverRatePercent = crossoverRatePercent;
    }

    public boolean isXoverNewAge() {
        return xoverNewAge;
    }

    public void setXoverNewAge(boolean xoverNewAge) {
        this.xoverNewAge = xoverNewAge;
    }
}
